#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SIZE 512
#define SEP " " //分隔符
#define NUM 32
#define ZERO '\0'
#define SkipPath(p) do{p+=(strlen(p)-1); while (*p != '/') p--; }while(0)

#define None_REDIR 0     //没有重定向
#define IN_REDIR 1       //输入重定向
#define OUT_REDIR 2      //输出重定向
#define APPEND_REDIR 3   //追加重定向
int redir_type = 0;
char* filename = NULL;
#define SkipSpace(commit, pos) do{                                           \
                                        while (commit[pos] == ' ') (pos)++;  \
                                 } while(0)

//命令行参数表
char* gArgv[NUM] = {NULL};//必须以NULL结尾
char cwd[SIZE*2] = {0};


const char* GetUsername()
{
    const char* ret = getenv("USER");//USER环境变量名
    if (ret == NULL) 
    {
        //perror("getenv");
        //exit(-1);
        return "None";
    }
    return ret;
};
const char* GetHostname()
{
    const char* ret = getenv("HOSTNAME");//HOSTNAME环境变量名
    if (ret == NULL) 
    {
        //perror("getenv");
        //exit(-1);
        return "None";
    }
    return ret;
};
const char* GetCwd()
{
    const char* ret = getenv("PWD");

    if (ret == NULL) 
    {
        //perror("getenv");
        //exit(-1);
        return "None";
    }
    return ret;
};
//11.
void MakeCommandAndPrint()
{
    //从环境变量中获取用户名, 主机名, 当前工作目录 (先封装成函数)
    const char* username = GetUsername();
    const char* hostname = GetHostname();
    const char* cwd = GetCwd();
    //printf("%s\n", cwd);
    SkipPath(cwd);//省略一些路径
    char line[SIZE] = {0};//缓冲区, 存放提示字符串, 先封装一下, 打印时直接打印文件就行
    snprintf(line, sizeof(line), "[%s@%s %s]>", username, hostname, strlen(cwd)==1 ? "/": cwd+1);
    printf("%s", line);
    //因为前面没有\n冲刷缓冲区, 所以手动fflush
    fflush(stdout);//标准输出流
    
}

//22.
int GetUserCommit(char commit[], size_t n)
{
    //从输入流中获取一行字符串 / 获取缓冲区所有字符
    const char* s = fgets(commit, n, stdin);//返回获取的str, 没获取到返回NULL
    if (s==NULL) return -1;
    commit[strlen(commit)-1] = ZERO;//消除(有效字符)最后一个回车,  注意: 如果不删除会影响指令选项的识别
    return strlen(commit);
}

//33.
void SplitCommit(char commit[], const char* sep)
{
    gArgv[0] = strtok(commit, sep);//返回分隔符(必须是字符串的形式)前一段首地址, 有记忆功能
    int i = 1;
    while ((gArgv[i++] = strtok(NULL, sep)));//根据上次的记录, 继续取字符段, 结束时返回NULL}

//    for (int i=0; gArgv[i]; i++)
//    {
//        printf("%d, %s\n", i, gArgv[i]);
//    }    
    
}

//nn.
void ExecCommit()
{
    pid_t id = fork();
    if (0 == id)
    {
        //child
        
        //重定向设置
        //输出重定向, 默认是向屏幕中输出 / 从屏幕中读取准备输入, 那我们把文件描述符1对应内容改为指定文件的地址, 就实现了向指定文件输出/读取
        if (filename != NULL)
        {
            if (redir_type == IN_REDIR)
            {
                int fd = open(filename, O_RDONLY);
                dup2(fd, 1);//修改1对应的内容
            }
            else if (redir_type == OUT_REDIR)
            {
                int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
                dup2(fd, 1);
            }
            else if (redir_type == APPEND_REDIR)
            {
                int fd = open(filename, O_APPEND | O_CREAT | O_WRONLY, 0666);
                dup2(fd, 1);
            }
            else 
            {}
            //在子进程中重置, 并没有重置父进程的
            //更新清空一下
            //filename = NULL;
            //redir_type = None_REDIR;
        }
        
        //开始替换
        execvp(gArgv[0], gArgv);//自动到环境变量PATH中找指令的路径
        exit(errno);//最后一次调用失败的错误码
    }
    else if (id > 0)
    {
        //father
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);//阻塞等待
        if (rid > 0)
        {
            if (WIFEXITED(status))
            {
                //正常执行
                int lastcode = WEXITSTATUS(status);
                if (lastcode != 0)//说明结果错误
                    printf("%s, %s\n", gArgv[0], strerror(lastcode));
            }
            else //WIFSIGNALED(status)
            {
                printf("exit_signal:%d\n", WTERMSIG(status));
            }
        }
        else 
        {
            //failed!!!
            perror("waitpid");
        }
    }
    else //-1
    {
        perror("fork");
    }
}

const char* GetHome()
{
    const char* path = getenv("HOME");
    if (path == NULL)   return "/";//没获取到就返回根目录
    return path;
}
void Cd()//用函数实现cd的功能, 让父进程执行
{
    const char* path = gArgv[1];//参数表中的目标路径
    if (path==NULL) path = GetHome();//没有目标路径时, 默认回家目录
    chdir(path);//更改当前进程的工作目录(重点)

    //更新PWD(环境变量, 用于记录当前工作目录)
    char temp[SIZE*2] = {0};
    //char cwd[SIZE*2] = {0};
    getcwd(temp, sizeof(temp));//C函数, 获取当前进程绝对路径, 避免路径. ..
    snprintf(cwd, sizeof(cwd), "PWD=%s", temp);//以这种格式将字符串写入(追加)cwd
    putenv(cwd);//增添/修改环境变量}
    //printf("1\n");
}
//44.
int CheckBuildin()
{
    int yes = 0;
    //只能一个一个的检查
    if (strcmp(gArgv[0], "cd") == 0)
    {
        yes = 1;
        Cd();
    }
    else if (strcmp(gArgv[0], "echo")==0 && strcmp(gArgv[1], "$?")==0)
    {
        yes = 1;
        printf("%d\n", errno);//最近一个程序的退出码
    }
    return yes;
}

//2.1.
void CheckRedir(char commit[])
{
    int pos = 0;
    int end = strlen(commit);
    while (pos < end)
    {
        if (commit[pos] == '>')
        {
            commit[pos++] = '\0';//直接截断, 前面是指令, 后面是文件名
            if (commit[pos] == '>')
            {
                pos++;
                redir_type = APPEND_REDIR;
            }
            else
                redir_type = OUT_REDIR;
            SkipSpace(commit, pos);//跳过空格
            filename = commit + pos;//系统在获取 文件名/指令时 会自动识别并舍弃后面多余的空格
        }
        else if (commit[pos] == '<')
        {
            commit[pos++] = '\0';
            redir_type = IN_REDIR;
            SkipSpace(commit, pos);
            filename = commit + pos;
        }
        else 
            pos++;
    }
}

void Debug(char commit[])
{
    printf("cmd: %s\n", commit);
    printf("redir: %d\n", redir_type);
    printf("filename: %s\n", filename);
}

int main()
{
    int quit = 0;//退出
    while (!quit)
    {
        //0.重置
        redir_type = None_REDIR;
        filename = NULL;
        //1.制作一个命令行并打印
        MakeCommandAndPrint();
        //2.获取命令行字符串
        char commit[SIZE] = {0};//写在外面, 是为了方便后面的拆分函数方便获取
        size_t n = sizeof(commit);
        int ret = GetUserCommit(commit, n);
        if (-1 == ret) return -1;//获取失败
        //2.1.检查是否有重定向
        CheckRedir(commit);
        //2.2. debug
        Debug(commit);
        //3.拆分解析字符串
        SplitCommit(commit, SEP);
        //4.检查是否是内建命令(由父进程执行)
        ret = CheckBuildin();
        if (ret)  continue;//是内建命令
        //n.执行程序
        ExecCommit();
    
    }
    
    return 0;
}
